Go 1.25 引入了一个令人兴奋的新特性:Trace Flight Recorder (飞行记录)。这个工具为 Go 开发者提供了一种更高效、更轻量级的生产环境调试和性能分析方法。本文将深入探讨 Trace Flight Recorder 的工作原理、配置方式、使用示例以及它在实际应用中的价值。

什么是 Tracing 和 Flight Recording?
在深入了解 Trace Flight Recorder 之前,我们首先需要理解两个核心概念:
Tracing (跟踪): Tracing 是一种监控和调试技术,通过收集程序执行的详细信息,例如函数调用、goroutine 活动、内存分配等,来帮助开发者识别性能瓶颈和调试复杂问题。传统的 tracing 方式通常会记录程序的整个生命周期,这可能会导致生成巨大的跟踪文件,带来较高的开销。
Flight Recording (飞行记录): 飞行记录是一种更精妙的跟踪方法。它不像传统跟踪那样捕获所有内容,而是在一个循环缓冲区中维护最新的执行数据。这意味着它只保留最近的程序活动,并自动丢弃较旧的信息,以节省空间并显著减少开销。这种方法特别适合在生产环境中持续运行,因为它只会产生一个大小可控的跟踪文件。
Trace Flight Recorder 的配置与使用
Go 1.25 的 trace.FlightRecorderConfig 结构体是配置 Trace Flight Recorder 的关键。它包含两个主要字段:
minAge: 用于指定跟踪事件在被丢弃之前至少保留的时长。例如,设置为 5 秒,则表示缓冲区中的数据至少会保留 5 秒。
maxBytes: 用于定义循环缓冲区的最大大小。例如,设置为 3MB,则表示缓冲区的大小上限为 3MB。
需要注意的是,这两个值是对 Go 运行时的建议,并不保证数据会被精确地保存。
实际代码演示
下面我们将通过一个简单的代码示例来演示如何使用 Trace Flight Recorder:
1. 创建一个简单的 Web 服务器
首先,我们创建一个基本的 HTTP 服务器,并添加一个 /heavy 路由来模拟 CPU 负载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| package main import ( "log" "net/http" "time" ) func pow(targetBits int) [32]byte{ target := big.NewInt(1) target.Lsh(target, uint(256-targetBits)) var hashInt big.Int var hash [32]byte nonce := 0 for { data := "hello world " + strconv.Itoa(nonce) hash = sha256.Sum256([]byte(data)) hashInt.SetBytes(hash[:]) if hashInt.Cmp(target) == -1 { break } else { nonce++ } if nonce%100 == 0 { runtime.Gosched() } } } func handler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/heavy" { heavyLoad() } w.Write([]byte("Hello, world!")) } func main() { http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe(":8080", nil)) }
|
2. 配置并启动 Trace Flight Recorder
接下来,我们在程序启动时配置并启动 Trace Flight Recorder。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package main import ( "log" "net/http" "time" "runtime/trace" "context" "os" ) var recorder *trace.FlightRecorder func main() { cfg := trace.FlightRecorderConfig{ MinAge: 5 * time.Second, MaxBytes: 3 * 1024 * 1024, } recorder = trace.StartFlightRecorder(cfg) defer recorder.Stop() http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe(":8080", nil)) }
|
3. 实现性能触发器并保存跟踪文件
在实际应用中,我们通常希望在发生异常或性能问题时自动保存跟踪文件。我们可以添加一个性能触发器,当计算出的hash值前6个字节都是0,自动保存跟踪快照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package main import ( "log" "net/http" "time" "runtime/trace" "os" ) func handlerWithTrigger(w http.ResponseWriter, r *http.Request) { hash := pow(rand.Intn(20) + 10) if strings.HasPrefix(hash, "000000") { file, err := os.Create("trace.out") if err != nil { log.Println("failed to create trace file:", err) return } defer file.Close() if _, err := recorder.WriteTo(file); err != nil { log.Println("failed to write trace data:", err) } } w.Write([]byte(hash)) } func main() { cfg := trace.FlightRecorderConfig{ MinAge: 5 * time.Second, MaxBytes: 3 * 1024 * 1024, } recorder = trace.NewFlightRecorder(cfg) if err := recorder.Start(); err != nil { log.Fatalf("failed to start FlightRecorder: %v", err) } defer recorder.Stop() http.HandleFunc("/", handlerWithTrigger) log.Fatal(http.ListenAndServe(":8080", nil)) }
|
在上面的例子中,我们使用 WriteTo 函数将循环缓冲区中的最新跟踪数据写入到 trace.out 文件中。
分析跟踪数据
保存跟踪文件后,我们可以使用 go tool trace 命令来分析它:
这个命令会打开一个浏览器页面,展示丰富的可视化数据(trace event),帮助你理解程序的执行情况。你可以看到:
- Goroutine 活动: 了解 goroutine 的创建、执行和阻塞情况。
- CPU 使用率: 查看不同逻辑处理器上的 CPU 使用模式。
- 堆使用模式: 分析内存分配和垃圾回收(GC)的模式。

用例与总结
Trace Flight Recorder 提供了在不影响性能的前提下,对 Go 应用程序进行持续监控和调试的能力。它的主要用例包括:
- 生产环境调试: 捕获围绕罕见或难以重现错误的上下文,而无需持续的完整跟踪。
- 性能监控: 分析那些只在特定条件下出现的不可预测的性能问题。
- 内存受限环境: 由于其低开销和可控的缓冲区大小,它非常适合在资源有限的设备上进行性能分析。
原理
这个新特性/新工具是在 issue#63185 中提出的。“飞行记录”是一种技术,其中将跟踪数据保存在一个概念上的环形缓冲区中,在请求时刷新。这种技术的目的是捕获有趣程序行为的跟踪,即使事先不知道何时会发生。例如,如果网络服务失败健康检查,或者网络服务处理请求的时间异常长。具体来说,网络服务可以在这些条件发生时识别它们,但设置环境的程序员无法预测它们何时会确切发生。在发生有趣的事情后开始跟踪通常也不太有用,因为程序已经执行了有趣的部分。
Java 生态系统已经通过 Java 的飞行记录器拥有这项功能多年了。一旦 JVM 的飞行记录器被启用,JVM 就可以获取代表最后几秒钟时间的跟踪信息。这个跟踪信息可以来自 JMX 中设置的触发器,或者通过传递一个标志给 JVM,在退出时导出跟踪信息。
随着 #60773 的实现逐渐接近稳定,Go 1.22 版本中我们能将所有跟踪信息变成一系列自包含的分区。这种实现变更提供了一个机会,可以轻松地添加类似于 Go 执行跟踪器的东西,通过始终保留至少一个可以在任何时间快照的分区。
这还得归功于 Go 1.21 版本中为了使跟踪成本大幅降低所做的努力。因为飞行记录依赖于等待有趣的事情发生,所以跟踪需要启用更长的时间。当跟踪本身并不昂贵时,在例如生产集群的小部分上启用飞行记录就变得更加容易接受。
设计核心是在 runtime/trace 包中引入了一个新的 API 以启用飞行记录。这意味着程序可以使用自己的触发器进行仪器化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package trace type FlightRecorder struct { ... } func NewFlightRecorder() *FlightRecorder func (*FlightRecorder) SetMinAge(d time.Duration) func (*FlightRecorder) MinAge() time.Duration func (*FlightRecorder) SetMaxBytes(bytes uint64) func (*FlightRecorder) MaxBytes() uint64 func (*FlightRecorder) Start() error func (*FlightRecorder) Stop() error func (*FlightRecorder) Enabled() bool func (*FlightRecorder) WriteTo(w io.Writer) (n int64, err error)
|
如果你在Go代码库中搜索 trace.ok()关键字,会看到很多跟踪的代码:

比如 trace.ProcSteal方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func (tl traceLocker) ProcSteal(pp *p, inSyscall bool) { mStolenFrom := pp.trace.mSyscallID pp.trace.mSyscallID = -1 if !pp.trace.statusWasTraced(tl.gen) && pp.trace.acquireStatus(tl.gen) { tl.writer().writeProcStatus(uint64(pp.id), tracev2.ProcSyscallAbandoned, pp.trace.inSweep).end() } goStatus := tracev2.GoRunning procStatus := tracev2.ProcRunning if inSyscall { goStatus = tracev2.GoSyscall procStatus = tracev2.ProcSyscallAbandoned } tl.eventWriter(goStatus, procStatus).event(tracev2.EvProcSteal, traceArg(pp.id), pp.trace.nextSeq(tl.gen), traceArg(mStolenFrom)) }
|
这个方法是 Go 运行时跟踪系统(runtime/trace)的一部分,它负责记录 Go 调度器中一个非常重要的事件:P 偷取(P stealing)。
这个 Go 代码片段是 Go 运行时跟踪系统(runtime/trace)的一部分,它负责记录 Go 调度器中一个非常重要的事件:P 偷取(P stealing)。
- 记录被偷取的 M
- 更新被偷取 P 的状态
- 确定偷取者的状态
- 发出主跟踪事件
tl.eventWriter(goStatus, procStatus).event(...): 这是最后一步,也是最重要的。它使用之前确定的状态,记录主事件 EvProcSteal。
- 该事件携带了关键信息:被偷取 P 的 ID(
pp.id)、一个事件序列号,以及被偷取 P 之前的 M 的 ID(mStolenFrom)。
当然全面的设计文档在 https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md, 这是Go 1.22中实现的基础性的工作,到Go 1.25中开始展示它的强大的功能。